Dungeons and Dragons Monsters

Author

Jen Richmond

Published

May 27, 2025

This week for TidyTuesday I decided to look at dragons within the dataset and practice writing a ggplot function that makes it easy to create the same plot for each level of a variable. In this case, I wanted a function that would spit me out a boxplot comparing the different dragon types on each of the key measures (strength, intelligence, wisdom, charisma, dexerity, and constitution).

load packages

Code
library(tidyverse)
library(tidytuesdayR)
library(janitor)
library(ggeasy)
library(patchwork)
library(Hmisc) # for %nin%

# adjust year/week values here
year = 2025
week = 21

get the data

Code

tt <- tt_load(year, week)

df <- tt[[1]]

head(df)
#> # A tibble: 6 × 33
#>   name    category    cr size  type  descriptive_tags alignment    ac initiative
#>   <chr>   <chr>    <dbl> <chr> <chr> <chr>            <chr>     <dbl>      <dbl>
#> 1 Aboleth Aboleth  10    Large Aber… <NA>             Lawful E…    17          7
#> 2 Air El… Air Ele…  5    Large Elem… <NA>             Neutral      15          5
#> 3 Animat… Animate…  1    Medi… Cons… <NA>             Unaligned    18          2
#> 4 Animat… Animate…  0.25 Small Cons… <NA>             Unaligned    17          4
#> 5 Animat… Animate…  2    Large Cons… <NA>             Unaligned    12          4
#> 6 Ankheg  Ankheg    2    Large Mons… <NA>             Unaligned    14          0
#> # ℹ 24 more variables: hp <chr>, hp_number <dbl>, speed <chr>,
#> #   speed_base_number <dbl>, str <dbl>, dex <dbl>, con <dbl>, int <dbl>,
#> #   wis <dbl>, cha <dbl>, str_save <dbl>, dex_save <dbl>, con_save <dbl>,
#> #   int_save <dbl>, wis_save <dbl>, cha_save <dbl>, skills <chr>,
#> #   resistances <chr>, vulnerabilities <chr>, immunities <chr>, gear <chr>,
#> #   senses <chr>, languages <chr>, full_text <chr>

make a plot theme

Code
# plot theme

theme_dragons <- function() { 
  
  theme_minimal() %+replace%
    theme(
    plot.title = element_text(size = 16, hjust = 0.05, 
                              colour = "black",  
                              margin = margin(b = 5)),
    plot.subtitle = element_text(size = 12, hjust = 0.05, 
                                 colour = "black",  
                                 margin = margin(b = 5)),
    plot.title.position = "plot",  # Position at the "plot" level rather than "panel"
    panel.background = element_rect(fill = "ghostwhite", color = NA),
    plot.background = element_rect(fill = "ghostwhite", color = NA),
    plot.margin = margin(t = 20, r = 20, b = 20, l = 20, unit = "pt")
    
      )
    
}
Code

# set the custom theme to apply to all plots in this document 

theme_set(theme_dragons())

wrangling

Here I am deciding to look at dragons. I need to filter out dragon types that only have a single row in the data. I am making the data long and adding a new column that is the mean_score on each of the variables for each dragon type (to allow for ordering later on).

Code

df %>%
  tabyl(type) %>%
  arrange(-n) %>%
  head()
#>         type  n    percent
#>        Beast 84 0.25454545
#>       Dragon 45 0.13636364
#>  Monstrosity 37 0.11212121
#>        Fiend 29 0.08787879
#>     Humanoid 26 0.07878788
#>       Undead 18 0.05454545

dragons <- df %>%
  filter(type == "Dragon") %>%
  select(name, category, type, str:cha) 

dragons %>%
  tabyl(category) %>%
   arrange(n) %>%
  head()
#>       category n    percent
#>  Dragon Turtle 1 0.02222222
#>    Half-Dragon 1 0.02222222
#>         Kobold 1 0.02222222
#>   Pseudodragon 1 0.02222222
#>         Wyvern 1 0.02222222
#>  Black Dragons 4 0.08888889

single_dragons <- c("Dragon Turtle", "Half-Dragon", "Kobold", "Pseudodragon", "Wyvern")

dragons_long <- dragons %>%
  filter(category %nin% single_dragons) %>%
  pivot_longer(names_to = "index", values_to = "score", str:cha) %>%
  mutate(measure = case_when(index == "str" ~ "Strength", 
                                  index == "dex" ~ "Dexterity", 
                                  index == "con" ~ "Constitution", 
                                  index == "int" ~ "Intelligence", 
                                  index == "wis" ~ "Wisdom", 
                                  index == "cha" ~ "Charisma", 
                                  ))

dragons_long <- dragons_long %>%
  group_by(category, index) %>%
  mutate(mean_score = mean(score))

make a plot

I make a basic plot using just the strength scores and work out how to order the boxplots by mean strength scores (see what i learned below for alternative options I played with).

Code
dragons_long %>% 
  filter(index == "str") %>%
    ggplot(aes(x = reorder(category, mean_score), y = score, fill = category)) +
    geom_boxplot(alpha = 0.5) +
    coord_flip() +
    labs(
      x = "Type of dragon",
      y = "Score",
      title = "Dragon Strength Scores"
    ) +
    easy_remove_legend()

write a function

Now to turn that code into a function. Here my function takes a dataframe and a value of the variable we are interested in (in this case index). It filters the data for that value, pulls out the value of measure (to use as a subtitle label), then plots the filtered data.

Code
plot_dragon_index <- function(data, variable) {
  filtered_data <- data %>%
    filter(index == variable) 
  
  measure <- filtered_data$measure[1]
  
  filtered_data %>%
    ggplot(aes(x = reorder(category, mean_score), y = score, fill = category)) +
    geom_boxplot(alpha = 0.5) +
    coord_flip() +
    labs(
      x = "Type of dragon",
      y = "Score",
      subtitle = paste(measure, "Scores")
    ) +
    easy_remove_legend()
}

test the function

Code

plot_dragon_index(dragons_long, "str")

combine plots

Code

plot_dragon_index(dragons_long, "str") + plot_dragon_index(dragons_long, "int") +
      plot_annotation(
             title = "Characteristics by Dragon Type",
              theme = theme(
      plot.title = element_text(
        hjust = 0,            # align left
        size = 16,
        color = "black",
        margin = margin(b = 5))  # spacing below title
    )
  )

Code
  
plot_dragon_index(dragons_long, "wis") + plot_dragon_index(dragons_long, "cha") +
  plot_annotation(
             title = "Characteristics by Dragon Type",
              theme = theme(
      plot.title = element_text(
        hjust = 0,            # align left
        size = 16,
        color = "black",
        margin = margin(b = 5))  # spacing below title
    )
  )

three things I learned…

1. theme_set()

You can make a custom ggplot theme and then automatically use it for all the plots in your Quarto Document using theme_set(). In this case, I called my theme theme_dragons()

Code
theme_set(theme_dragons())

2. plot_annotation

When using the patchwork package to combine plots you can use plot_annotation() to add titles etc to your combined plot. You can also use theme() functions, in much the same way you would for ggplot to control your title, margins or background. See patchwork vignette.

Code

plot1 + plot2 +
    plot_annotation(title = "My title",
              theme = 
                theme(plot.background = element_rect(fill = "ghostwhite"),
                      plot.margin = margin(20, 20, 20, 20),  # T, R, B, L 
                      plot.title = element_text(hjust = 0, # align left
                                  size = 16,  color = "black",  
                      margin = margin(b = 5))  # spacing below title
    )
  )

3. how to reorder x axis in ggplot

coorid_flip() is handy when you want x axis labels to display more clearly, but it is a bit aanoying that the order L-R gets translated B-T. I played for a little bit working out how to use fct_rev() to make the dragons display in alphabetical order from top to bottom, before deciding that ordering the bars by the mean_score would make for a more meaningful plot. Here is the code for each of those options in a panel-tabset.

Code
dragons_long %>% 
  filter(index == "str") %>%
    ggplot(aes(x = category, y = score)) +
   geom_boxplot(fill = "lavender") 

Code
dragons_long %>% 
  filter(index == "str") %>%
    ggplot(aes(x = category, y = score)) +
   geom_boxplot(fill = "lavender")  +
  coord_flip() 

Code

dragons_long %>% 
  filter(index == "str") %>%
    ggplot(aes(x = fct_rev(category), y = score)) +
  geom_boxplot(fill = "lavender") +
    coord_flip() 

Code

dragons_long %>% 
  filter(index == "str") %>%
    ggplot(aes(x = reorder(category,mean_score), y = score)) +
 geom_boxplot(fill = "lavender") +    
  coord_flip()